---
title: "Tooltips for the headers of a datatable in Shiny"
author: "Stéphane Laurent"
date: '2020-02-16'
tags: R, datatables, javascript, shiny
rbloggers: yes
output:
md_document:
variant: markdown
preserve_yaml: true
html_document:
highlight: kate
keep_md: no
highlighter: pandoc-solarized
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE, collapse = TRUE)
```
In this post, I show how to use the
[qTip2](http://qtip2.com/) JavaScript library to create some tooltips on the
headers of a `DT` datatable in Shiny, displaying some information about the
columns, such as summary statistics.
Firstly, we write a function returning some JavaScript code that creates
some hidden `div` elements which will contain the contents of the tooltips.
This function takes two arguments: `n`, the number of `div` elements to create
(this will be the number of columns of the table), and `prefixID`; the i-th
`div` will have the identifier `{prefixID}-{i}`. We also set a class to
each `div`, namely `qtip-big`.
```{r, attr.source='.numberLines'}
createDiv <- function(n, prefixID){
sprintf(paste(
"for(var i = 1; i <= %d; i++){",
" var div;",
sprintf(" var id = '%s-' + i.toString();", prefixID),
" if(document.getElementById(id) === null){",
" div = document.createElement('div');",
" div.setAttribute('id', id);",
" div.setAttribute('class', 'qtip-big');",
" div.style.display = 'none';",
" document.body.appendChild(div);",
" }",
"}",
sep = "\n"
), n)
}
```
```{r}
cat(createDiv(3, "TOOLTIP"))
```
Now we write a function returning some JavaScript code that writes the
contents of the `div` elements. It takes as arguments `dat`, the dataframe for
the table, `i`, the index of a column of `dat`, and `prefixID` as in the
function `createDiv`.
If the `i`-th column is numeric, we fill the `div` element with some summary
statistics of this column, otherwise the information we provide in the `div`
element is the number of levels of the contents of the column, an enumeration
of the levels (at most three), and the number of missing values.
```{r, attr.source='.numberLines'}
fillDiv <- function(dat, i, prefixID){
x <- dat[[i]]
if(is.numeric(x)){
sprintf(paste(
"var div = document.getElementById('%s-%d');",
"var html = ' Min: %s';",
"html = html + ' Max: %s';",
"html = html + ' Mean: %s';",
"html = html + ' Std. dev.: %s';",
"html = html + ' Missing values: %d';",
"div.innerHTML = html;",
sep = "\n"
),
prefixID,
i,
formatC(min(x, na.rm=TRUE)),
formatC(max(x, na.rm=TRUE)),
formatC(mean(x, na.rm=TRUE)),
formatC(sd(x, na.rm=TRUE)),
sum(is.na(x)))
}else{
if(is.factor(x)) x <- as.character(x)
levels0 <- sort(unique(na.omit(x)))
nlevels <- length(levels0)
levels <-
if(nlevels>3) c(levels0[1:2], levels0[nlevels]) else levels0
ncharMax <- 25
nchars <- nchar(levels)
if(7+sum(nchars) > ncharMax){
levels[1] <- paste0("
", levels[1])
if(nlevels >= 2 && sum(nchars) > ncharMax){
levels[2] <- paste0("
", levels[2])
if(nlevels >= 3 && sum(nchars[2:3]) > ncharMax){
levels[3] <- paste0("
", levels[3])
}
}
}
levelsSummary <- ifelse(nlevels > 3,
paste0(c(levels[1],
paste0(levels[2], ", ..."),
levels[3]),
collapse = ", "),
paste0(levels, collapse = ", "))
sprintf(paste(
"var div = document.getElementById('%s-%d');",
"var html = ' Number of levels: %d';",
sprintf("html = html + ' Level%s: %%s'",
ifelse(nlevels==1, "", "s")),
"html = html + ' Missing values: %d'",
"div.innerHTML = html;",
sep = "\n"
),
prefixID,
i,
nlevels,
levelsSummary,
sum(is.na(x)))
}
}
```
```{r}
cat(fillDiv(iris, 1, "TOOLTIP"))
cat(fillDiv(iris, 5, "TOOLTIP"))
```
Finally we write a function returning the JavaScript code of the `qTip`
tooltips. Its arguments are `n`, the number of columns of the table, and
`prefixID` as before.
```{r, attr.source='.numberLines'}
tooltips <- function(n, prefixID){
settings <- sprintf(paste(
"{",
" overwrite: true,",
" content: {",
sprintf(" text: $('#%s-%%s').clone()", prefixID),
" },",
" show: {",
" ready: false",
" },",
" position: {",
" my: 'bottom %%s',",
" at: 'top center'",
" },",
" style: {",
" classes: 'qtip-youtube'",
" }",
"}",
sep = "\n"
), 1:n)
settings <- sprintf(settings, ifelse(1:n > n/2, "right", "left"))
sprintf("var tooltips = [%s];", paste0(settings, collapse=","))
}
```
```{r}
cat(tooltips(2, "TOOLTIP"))
```
Now we are ready to write the Shiny app.
Put the files **jquery.qtip.min.css** and **jquery.qtip.min.js** in the **www**
subfolder. We use the `shinyjs` package to run the JavaScript code with the
function `runjs`.
```{r, attr.source='.numberLines', eval=FALSE}
library(shiny)
library(shinyjs)
library(DT)
CSS <- "
.qtip-big {
font-size: 15px;
line-height: 18px;
white-space: nowrap;
word-spacing: 1px;
}
"
ui <- fluidPage(
tags$head(
tags$link(rel = "stylesheet", href = "jquery.qtip.min.css"),
tags$script(src = "jquery.qtip.min.js"),
tags$style(CSS)
),
useShinyjs(),
br(), br(), br(), br(), br(),
DTOutput("dtable")
)
server <- function(input, output, session){
output[["dtable"]] <- renderDT({
dat <- iris
for(i in 1:ncol(dat)){
runjs(createDiv(i, "TOOLTIP"))
runjs(fillDiv(dat, i, "TOOLTIP"))
}
headerCallback <- c(
"function(thead, data, start, end, display){",
" var ncols = data[0].length;",
tooltips(ncol(dat), "TOOLTIP"),
" for(var i = 1; i < ncols; i++){",
" $('th:eq(' + i + ')', thead).qtip(tooltips[i-1]);",
" }",
"}"
)
datatable(
dat,
options = list(
headerCallback = JS(headerCallback),
columnDefs = list(
list(className = "dt-center", targets = "_all")
)
)
)
})
}
shinyApp(ui, server)
```
![](./figures/DTqTip.gif)